from functools import wraps
import warnings
import inspect
import numpy as np
from numpy import array, zeros, dtype
from numpy.core.records import recarray
from pdb import set_trace
# TODO name module data_object
# TODO create a class to store raw data RawDataBase
# TODO create class to store results ResultsDataBase
# TODO create a MetaDataBase 
# TODO create a class DataObject that inherits PrmDictBase, RawDataBase, and ResultsBase
# TODO should the DataObject should have to_array
"""
Module for the creation of a flexible data object to use for 
measurements.
classes:
    PrmDictBase(dict)
    RawDataBase(numpy array)
    ResultsDataBase(dict)
    DataObject(PrmDictBase, RawData, DataObject)
"""

def _ex_args(f):
    """
    Function decorator that sets values of the named arguments of the
    decorated function to values supplied by a dictionary passed as a keyword
    argument, args.
    f : function with named arguments (e.g. f(a, b, c=1, d=2))
    return new_func : function that has keyword arguments substituted

    example:
    @_ex_args
    def f(a, b, c=1, d=2, e=3)

    
                    1. The function definition arguments are placed in a new dictionary
                    2. Any keys in any of the args that match a key in the new dictionary
                       are replaced by the value from the arg.
                    3. Finally, all kwargs are used to update the new dictionary
                    4. The function is then called with the new dictionary
    """
    required_args = inspect.getargspec(f).args
    @wraps(f)
    def new_func(*args, **kwargs):
        if kwargs.has_key('args'):
            passed_args = kwargs.pop('args')
            new_args = dict([k,passed_args[k]] for k in required_args \
                    if passed_args.has_key(k))
        else:
            default_vals = inspect.getargspec(f).defaults
            new_args = dict(zip(required_args, default_vals))
        new_args.update(kwargs)
        return f(**new_args)
    return new_func

class PrmDictError(Exception):
    """
    Exception to define errors in that occur
    in the PrmDictBase class
    """
    def __init__(self, message):
         self.message = message
    def __str__(self):
         return repr(self.message)

def _argument_checker(f):
    """
    Decorator function used to perform checks on arguments
    of methods that change values in the PrmDictBase object.
    """
    def _confirm_type(param, new_val, old_val):
        """
        Confirm that the value of new_val is of the same type
        as old_val. If new_val is a list or tuple, recurse on the
        elements to confirm all elements are of same type. 
        param - string name of parameter
        new_val - new value of the parameter
        old_val - pervious value of the paramter
        """
        if old_val is None:
            valid = True
        elif type(new_val) != type(old_val):
            valid = False
            raise PrmDictError("Value type of paramter %s changed" %param)
        elif (type(new_val) == type(old_val)) and \
            (not isinstance(new_val, (list, tuple))):
            valid = True
        elif (isinstance(new_val, (list, tuple))) and \
                (len(new_val) != len(old_val)):
            valid = False
            raise PrmDictError("Length of paramter %s changed" %param)
        elif (isinstance(new_val, (list, tuple))) and \
                (len(new_val) == len(old_val)):
            set_trace()
            for v_old, v_new in zip(new_val, old_val):
               valid = _confirm_type(param, v_old, v_new)
               if not valid:
                   raise PrmDictError("Value type in parameter %s changed" %param)
                   break
        else:
            raise TypeError("Cannot handle parameter values for parameter %s" %param)
        return valid

    def _check_args(old_dict, new_dict):
        """
        Check to ensure that keys in the new_dict are defined in the old_dict.
        If the key has been defined, then validate that
        the new parameter value is of the same type as the old parameter
        value's type.
        old_dict - PrmDictBase object
        new_dict - dictionary
        """
        for new_param, new_val in new_dict.iteritems():
            if new_param in old_dict:
                try:
                    check = _confirm_type(new_param, new_val, old_dict[new_param])
                except PrmDictError:
                    raise 
            else:
                raise PrmDictError("No such parameter: %s" %new_param)
        return check

    @wraps(f)
    def dec_method(self, *args, **kwargs):
        """
        The decorated function that calls the base class method.
        """
        new_dict = {}

        if len(args) == 1:
            try:
                new_dict.update(args[0])
            except:
                pass
        elif len(args) == 2:
                new_dict[args[0]] = args[1]

        if len(kwargs) > 0:
            new_dict.update(kwargs)
            
        valid = _check_args(self, new_dict)

        if valid:
            f(self, *args, **kwargs)

    return dec_method

class PrmDictBase(dict):
    """
    Base clase for a parameter dictionary. The parameter dictionary
    should contain all object parameters. The data should be contained
    in the data object
    """
    def __init__(self, *args, **kwargs):
        """
        Call dict base class initiallization passing
        a possible dictionary object, p_dict.
        """
        if len(args) == 1 and len(kwargs) == 0:
            super(PrmDictBase, self).__init__(args[0])
        elif len(args) == 0 and len(kwargs) > 0:
            super(PrmDictBase, self).__init__(kwargs)
        else: 
            raise TypeError("Improper arguments")

    @_argument_checker
    def __setitem__(self, k, v):
        """
        Sets the value of item, k to value v. This method overrides the
        dictionary update with the additional functionality of asserting the
        key must already exist and checking that and updated values for each
        key have the same type as the current values for the key.
        
        """
        super(PrmDictBase, self).__setitem__(k, v)

    @_argument_checker
    def update(self, *args, **kwargs):
        """
        Update the values in the dictionary. This method overrides the
        dictionary update with the additional functionality of asserting the
        key must already exist and checking that and updated values for each
        key have the same type as the current values for the key.
        """
        super(PrmDictBase, self).update(*args, **kwargs)

    def to_array(self):
        """
        Create a numpy structured array of the parameter list
        """
        strtype = lambda o: type(o).__name__

        type_map = {'str':'|S30', 'int':'int32', 'float':'float64'}

        pdtype = []
        for k, v in self.iteritems():
            if strtype(v) != 'tuple' and strtype(v) != 'list':
                pdtype.append((k, type_map[strtype(v)]))
            elif strtype(v) != 'tuple' or strtype(v) != 'list':
                set_trace()
                pdtype.append((k, type_map[strtype(v[0])], len(v)))
            else:
                raise TypeError("Cannot convert type of %s to numpy array" %k)

        starray = zeros((1), dtype=pdtype)
        for k, v in self.iteritems():
            starray[k] = v

        return starray

    def to_hdf5(self, fname, hdf5_path, name):
        """
        Write the parameter list to an HDF5 file.
        fname - hdf5 filename
        hdf5_path - path to create group in the hdf5 file to save data
        name - string name to create group under hdf5_path for parameters
        """
        if hdf5_path[0] != '/':
            hdf5_path = '/' + hdf5_path
        if hdf5_path[-1] != '/':
            hdf5_path = hdf5_path + '/'
        if name[0] == '/':
            name = name[1:]
        if name[-1] != '/':
            name = name + '/'
        full_name = hdf5_path + name

        with File(fname, mode='a') as f_h5py:
            f_h5py.create_group(full_name)

            for k, v in self.iteritems():
                if v is None:
                    v = "None"
                f_h5py.create_dataset(full_name+k, data=v)

def array_to_dict(str_array):
    """
    Convert a numpy structured array into a dictionary.
    str_array : numpy structured array
    return : dictionary with keys equal to numpy names and values equal to
        numpy values
    """
    dict_array = {}

    for nm in str_array.dtype.names:
        if len(str_array[nm].dtype.descr) > 1:
            t = array_to_dict(str_array[nm])
            dict_array.update(t)
        else:
            try:
                if len(str_array[nm]) == 1:
                    # Make arrays of len exacty 1, a scalar
                    dict_array[nm] = str_array[nm][0]
                else:
                    dict_array[nm] = str_array[nm]
            except TypeError:
                dict_array[nm] = str_array[nm]

    return dict_array

def array_to_dict_gen(str_array):
    """
    Convert a numpy structured array into a dictionary.
    str_array : numpy structured array
    return : dictionary with keys equal to numpy names and values equal to
        numpy values
    """
    dict_array = {}

    for i in xrange(len(str_array)):
        for k in str_array.dtype.names:
            dict_array[k] = str_array[k][i]
        yield dict_array

def dtype_from_descr(descr):
    """
    Create a numpy dtype object from the output
    of the array.dtype.descr.
    descr : list output of numpy.array.dtye.descr
    """
    names, titles, formats = [], [], []

    for d in descr:
        if isinstance(d[1], list): 
            if isinstance(d[0], tuple):
                names.append(d[0][1])
                titles.append(d[0][0])
            else:
                names.append(d[0])
                titles.append('t'+d[0])
            formats.append(dtype_from_descr(d[1]))
        else:
            if isinstance(d[0], tuple):
                names.append(d[0][1])
                titles.append(d[0][0])
            else:
                names.append(d[0])
                titles.append('t'+d[0])
            if len(d) == 2:
                formats.append(d[1])
            else:
                formats.append(str(d[2])+d[1].replace("<",""))

    dat_type = np.dtype({'names':names, 'titles':titles, 'formats':formats})

    return dat_type

def append_field(str_array, tuple_descrs, data):
    """
    Append a field or fields to a number structured array.
    str_array : numpy structured array to append fields to
    tuple_descrs : list of tuple descriptions [(('title', 'name'), 'format', ['size'])]
    data : list of data values
    """
    descr = str_array.dtype.descr
    for t in tuple_descrs:
        descr.append(t)
    new_array = np.zeros(len(str_array), dtype=dtype_from_descr(descr))
    for k in str_array.dtype.names:
        new_array[k] = str_array[k]

    for i, [k, v] in enumerate(zip(tuple_descrs, data)):
        new_array[k[0][1]] = data[:,i]
    return new_array
 
def merge_dicts(*args):
    result = {}
    for x in args:
      result.update(x)
    return result
